package com.andox.fn.inf.rpc.rest.filter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import com.andox.fn.inf.rpc.rest.RequestContext;
import com.andox.fn.inf.rpc.rest.RequestContextConstants;
import com.andox.fn.inf.rpc.rest.ResponseCodes;
import com.andox.fn.inf.rpc.rest.RpcRequest;
import com.andox.fn.inf.rpc.rest.RpcResponse;
import com.andox.fn.inf.rpc.rest.common.LifeCycle;
import com.andox.fn.inf.rpc.rest.common.RequestType;
import com.andox.fn.inf.rpc.rest.config.InterfaceConfig;
import com.andox.fn.inf.rpc.rest.config.MethodConfig;
import com.andox.fn.inf.rpc.rest.config.ServiceConfig;
import com.andox.fn.inf.rpc.rest.exception.ServiceRuntimeException;
import com.andox.fn.inf.rpc.rest.extension.ExtensionLoader;
import com.andox.fn.inf.rpc.rest.serializer.Serializer;
import com.andox.fn.inf.rpc.rest.util.IfNullUtil;

public class RestInvoker implements Invoker, LifeCycle{
	private final static Logger logger = LoggerFactory.getLogger(RestInvoker.class);

	private ServiceConfig serviceConfig;
	
	private Serializer serializer;
	
	private Map<String /** uri **/, Method> methodCache = Maps.newConcurrentMap();
	private Set<String> excludeSet = Sets.newHashSet();
	
	public RestInvoker(ServiceConfig serviceConfig) {
		this.serviceConfig = serviceConfig;
		serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(serviceConfig.getSerialization());
	}
	
	private void init() {
		Preconditions.checkArgument(serviceConfig != null, "serviceConfig is null");
		Preconditions.checkArgument(serviceConfig.getInterfaceClass() != null, "interfaceClass is null");
		Preconditions.checkArgument(serviceConfig.getRef() != null, "ref is null");
		
		Set<String> excludes = IfNullUtil.ifNull(serviceConfig.getMethodConfigs())
				.stream()
					.filter(MethodConfig::isExclude)
					.map(MethodConfig::getName)
				.collect(Collectors.toSet());
		this.excludeSet = excludes;
		
		Method[] methods = serviceConfig.getInterfaceClass().getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (!method.getDeclaringClass().equals(Object.class)) {
				String url = serviceConfig.getInterfaceClass().getName() + "/" + method.getName();
				Parameter[] ps = method.getParameters();
				for (int j = 0; j < ps.length; j++) {
					Parameter p = ps[j];
					url = url + "/" + p.getType().getName();
				}
				
				logger.info("url: {}, method: {}", url, method.getName());
				this.methodCache.put(url, method);
			}
		}
	}

	public RpcResponse invoke(RpcRequest ar, HttpServletRequest httpServletRequest) {
		RpcResponse response = new RpcResponse(ar.getOpaque());
		try {
			checkExclude(ar);
			checkUniqueId(ar);
			
			String uri = httpServletRequest.getRequestURI();
			uri = this.normalizeUri(uri);
			
			Method method = (Method) methodCache.get(uri);
			if (method == null) {
				logger.error("method is not correct,method=", method);
				response.setCode("2");
				response.setMsg("method is not correct, request method is null");
				return response;
			}

			Object result = null;
			Class<?>[] parameterTypes = method.getParameterTypes();
			if (parameterTypes.length == 0) {
				result = method.invoke(serviceConfig.getRef(), new java.lang.Object[0]);
			} else {
				@SuppressWarnings("unchecked")
				Map<String, String> params = JSON.parseObject(ar.getData(), Map.class);
				if (params == null)
					params = new java.util.HashMap<>();
				Object[] ps = paramsConvert(params, method);
				result = method.invoke(serviceConfig.getRef(), ps);
			}

			response.setData(this.serializer.serializeAsString(result));
		} catch (InvocationTargetException e) {
			logger.error("InvocationTargetException, {}", e);
			response.setCode(ResponseCodes.TRANLATOR_ERROR);
			response.setMsg("InvocationTargetException");
		} catch (IllegalAccessException e) {
			logger.error("IllegalAccessException, {}", e);
			response.setCode(ResponseCodes.TRANLATOR_ERROR);
			response.setMsg("IllegalAccessException");
		} catch (ServiceRuntimeException e) {
			response.setCode(ResponseCodes.TRANLATOR_ERROR);
			response.setMsg(e.getMessage());
			logger.error("serviceruntimeexception, {}", e);
		} catch (Exception e) {
			response.setCode(ResponseCodes.BIZ_ERROR);
			response.setMsg("biz exception, " + e.getMessage());
			logger.error("exception, {}", e);
		}
		
		return response;
	};

	private String normalizeUri(String uri) {
		return StringUtils.strip(uri, "/");
	}
	
	private void checkUniqueId(RpcRequest request) {
		if (!StringUtils.isBlank(request.getUniqueId())
				&& !StringUtils.isBlank(this.serviceConfig.getUniqueId())
				&& !StringUtils.equals(request.getUniqueId(), serviceConfig.getUniqueId())) {
			String msg = String.format("uniqueid do not match, server uniqueid is %s, client uniqueid is %s", 
					serviceConfig.getUniqueId(), request.getUniqueId());
			throw new ServiceRuntimeException(msg);
		}
	}
	
	private void checkExclude(RpcRequest request) {
		if (this.excludeSet.contains(request.getMethod())) {
			String msg = String.format("the method [%s.%s] do not exported , please contact to devoloper of the server.", 
					serviceConfig.getInterfaceClass().getName(), request.getMethod());
			throw new ServiceRuntimeException(msg);
		}
	}
	
	@SuppressWarnings("unused")
	private Object heartbeat(RpcRequest ar) {
		if (ar.getType() == RequestType.HEART_BEAT) {
			
		}
		
		return "true";
	}
	
	private Object[] paramsConvert(Map<String, String> map, Method method) {
		Parameter[] parameters = method.getParameters();
		if (parameters == null || parameters.length == 0) {
			return null;
		} else {
			List<Object> resultList = new java.util.ArrayList<>();
			if (map.size() != parameters.length)
				throw new ServiceRuntimeException("参数不匹配");

			for (int i = 0; i < parameters.length; i++) {
				Class<?> paramType = parameters[i].getType();
				String index = String.valueOf(i);
				Object x = map.get(index);
				Object result = null;
				if (null != x) {
					result = serializer.unserializeFromString((String)x, paramType);
					
					// classloader error 
					if (result != null) {
						ClassLoader x1 = result.getClass().getClassLoader();
						ClassLoader x2 = paramType.getClassLoader();
						if (x1 != null && x2 != null && 
								!x1.equals(x2)) {
							String classloadererror = 
								String.format("Unserialized instance %s classloader is %s, Type Class %s classlaoder is %s, classloader modified.",
									result.getClass().getName(), result.getClass().getClassLoader().getClass().getName(),
									paramType.getName(), paramType.getClassLoader().getClass().getName());
							logger.error(classloadererror);
							System.err.println(classloadererror);
							throw new ServiceRuntimeException(classloadererror);
						}
					}
				}
				resultList.add(result);
			}

			return resultList.toArray();
		}
	}

	@Override
	public void start() {
		this.init();
	}

	@Override
	public void close() {
		
	}

	@Override
	public InterfaceConfig getConfig() {
		return this.serviceConfig;
	}

	@Override
	public RpcResponse invoke(RpcRequest request) {
		HttpServletRequest httpServletRequest = (HttpServletRequest)RequestContext.instance().getProperty(RequestContextConstants.HTTP_SERVLET_REQUEST);
		return invoke(request, httpServletRequest);
	}
}
